package com.vhall.component.framework.core.environment;

import com.vhall.component.framework.common.annotation.VhallComponent;
import com.vhall.component.framework.common.utils.StringUtils;
import com.vhall.component.framework.core.loader.CustomerExtension;
import com.vhall.component.framework.core.loader.ExtensionBinder;
import com.vhall.component.framework.core.loader.ExtensionHelper;
import com.vhall.component.framework.feignclient.core.FeignProxyController;
import com.vhall.component.framework.feignclient.core.FeignProxyInvocationHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.io.FileUrlResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.*;

/**
 * 动态创建 Bean 并注入到 BeanFactory 中，在此期间创建并加载配置 customer 配置文件
 *
 * @author xiaoliang.wang@vhall.com
 */
@Slf4j
@Component
public class PropertyComponentSourceDetectorConfiguration implements ImportBeanDefinitionRegistrar,
        EnvironmentAware, BeanFactoryAware, InitializingBean {

    /**
     * 插件列表
     */
    private static List<CustomerExtension> extensions = null;

    /**
     * 插件对应的 feign 接口
     */
    private static Set<Class<?>> extensionFeign = new HashSet<>();

    /**
     * bean factory
     */
    private DefaultListableBeanFactory beanFactory;

    /**
     * environments for spring context
     */
    private ConfigurableEnvironment environment;

    /**
     * 初始化自定义配置文件，并初始化扩展插件，注册到 bean 中
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
                                        BeanDefinitionRegistry registry) {
        registerPluginHandler();
    }

    /**
     * 初始化 environment, 同时对外部配置文件进行初始化加载
     * 初始化配置文件加载器
     */
    protected void loadComponentsEnvironment() {
        // 激活的 profiles
        String[] activeProfiles = environment.getActiveProfiles();
        ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        try {
            PropertySourceLoaderComposite resourcesLoader = new PropertySourceLoaderComposite();
            // 读取各环境下的配置文件合并到 environment 中
            Iterator<CustomerExtension> iterator = extensions.iterator();
            while (iterator.hasNext()) {
                CustomerExtension extension = iterator.next();
                for (String profile : activeProfiles) {
                    String classpath = extension.getClassPath(profile);
                    Resource[] resources = resourcePatternResolver.getResources(classpath);
                    for (Resource resource : resources) {
                        resourcesLoader.load(environment, resource);
                    }
                }
            }

            // 加载外部配置文件到环境中
            String value = environment.getProperty("spring.conf.ext");
            if (Objects.isNull(value)) {
                return;
            }

            // 将外部分配置对当前环境进行覆盖
            Resource ext = new FileUrlResource(value);
            if (ext.exists()) {
                resourcesLoader.load(environment, ext);
            }
            // 按应用加载外部配置文件中定义的过滤器，并覆盖当前 extensions 的插件配置
            Binder.get(environment).bind("extension", ExtensionBinder.class).ifBound(binders -> {
                binders.rebuild(extensions);
            });
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }

    }

    /**
     * 初始化插件列表
     */
    protected void registerPluginHandler() {
        // 初始化属性解析器
        if (!beanFactory.hasEmbeddedValueResolver()) {
            beanFactory.addEmbeddedValueResolver(strVal -> environment.resolvePlaceholders(strVal));
        }

        for (CustomerExtension extension : extensions) {
            for (Class<?> targetClass : extension.getClasses()) {
                VhallComponent component = targetClass.getAnnotation(VhallComponent.class);
                if (Objects.isNull(component)) {
                    throw new RuntimeException("@VhallComponent not found");
                }
                // 创建 plugin handler
                initHandlerBeanDefinition(targetClass, targetClass.getName());
            }
        }
    }

    /**
     * 创建 PluginHandler, 对属性进行注入操作
     * 这里只能注入 @ValueEnhance 相关的注解，
     *
     * @param targetClass
     * @param beanName
     * @return
     * @AutoWired 与 @Value 注解会在 Bean 注册完成后，
     * 最终由 AutowiredAnnotationBeanPostProcessor 处理
     */
    protected void initHandlerBeanDefinition(Class<?> targetClass, String beanName) {
        Class<?>[] interfaces = targetClass.getInterfaces();
        Assert.notEmpty(interfaces, targetClass.getName() + "must be implements @FeignClient interface");

        for (Class<?> interfaceClass : interfaces) {
            if (interfaceClass.isAnnotationPresent(FeignClient.class)) {
                BeanDefinition feignDefinition = beanFactory.getBeanDefinition(interfaceClass.getName());
                feignDefinition.setPrimary(false);
                extensionFeign.add(interfaceClass);
            }
        }

        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(targetClass);
        builder.setPrimary(true);
        builder.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
        builder.setDependencyCheck(AbstractBeanDefinition.DEPENDENCY_CHECK_NONE);
        beanFactory.registerBeanDefinition(beanName, builder.getBeanDefinition());
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = (DefaultListableBeanFactory) beanFactory;
        this.environment = beanFactory.getBean(ConfigurableEnvironment.class);
    }

    @Override
    public void setEnvironment(Environment environment) {
        if (Objects.isNull(extensions)) {
            extensions = ExtensionHelper.loadComponentsConfig();
            loadComponentsEnvironment();
        }
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        for (Class<?> interfaceClass : extensionFeign) {
            try {
                buildRequestMapping(interfaceClass);
                buildMvcController(interfaceClass);
            } catch (Exception e) {
                log.error("", e);
            }
        }
    }

    /**
     * 代理 controller 层
     *
     * @param interfaceClass
     */
    private void buildMvcController(Class<?> interfaceClass) {
        Object bean = beanFactory.getBean(interfaceClass);
        if (bean != null) {
            Enhancer enhancer = new Enhancer();
            enhancer.setCallback(new FeignProxyInvocationHandler(bean));
            enhancer.setInterfaces(new Class[]{interfaceClass, FeignProxyController.class});
            Object proxy = enhancer.create();
            beanFactory.registerSingleton(org.springframework.util.StringUtils.capitalize(interfaceClass.getSimpleName()) + "$FeignProxyController", proxy);
        }
    }

    /**
     * 修改 requestMapping path 属性
     *
     * @param interfaceClass
     * @throws Exception
     */
    private void buildRequestMapping(Class<?> interfaceClass) throws Exception {
        FeignClient feignClient = interfaceClass.getAnnotation(FeignClient.class);
        String path = feignClient.path();
        if (StringUtils.isNotEmpty(path)) {
            Method[] methods = interfaceClass.getMethods();
            for (Method method : methods) {
                RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
                if (Objects.nonNull(requestMapping)) {
                    parseRequestMapping(requestMapping, path, requestMapping.value());
                    continue;
                }
                GetMapping getMapping = method.getAnnotation(GetMapping.class);
                if (Objects.nonNull(getMapping)) {
                    parseRequestMapping(getMapping, path, getMapping.value());
                    continue;
                }
                PostMapping postMapping = method.getAnnotation(PostMapping.class);
                if (Objects.nonNull(postMapping)) {
                    parseRequestMapping(postMapping, path, postMapping.value());
                    continue;
                }
            }
        }
    }

    @SuppressWarnings("unchecked")
    private void parseRequestMapping(Object annotationType, String path, String[] values) throws Exception {
        if (Objects.nonNull(values)) {
            InvocationHandler invocationHandler = Proxy.getInvocationHandler(annotationType);
            Field f = invocationHandler.getClass().getDeclaredField("memberValues");
            f.setAccessible(true);
            Map<String, Object> memberValues = (Map<String, Object>) f.get(invocationHandler);
            for (int i = 0; i < values.length; i++) {
                values[i] = path + values[i];
            }
            memberValues.put("value", values);
        }
    }

}
